Skip to content

improve startup perf - #1030

Open
tandraschko wants to merge 3 commits into
apache:mainfrom
tandraschko:startupperf
Open

improve startup perf#1030
tandraschko wants to merge 3 commits into
apache:mainfrom
tandraschko:startupperf

Conversation

@tandraschko

@tandraschko tandraschko commented Jul 25, 2026

Copy link
Copy Markdown
Member

What

A set of internal, behavior-preserving optimizations that reduce web application
deployment/startup time. There are no changes to defaults, configuration,
public API, parsing output, or threading model
— outputs are identical; only
redundant work and allocations are removed.

Changes

Annotation / JAR scanning (ContextConfig)

  • Index the @HandlesTypes types by name (class-file descriptor form for
    annotations, binary name for classes) so matching a scanned class is an O(1)
    lookup instead of a linear scan of all handled types plus a per-annotation
    string conversion.
  • Record whether a fragment JAR contains META-INF/resources/ during the
    annotation scan (which already iterates every entry), so processResourceJARs
    no longer re-opens and re-scans each JAR.
  • Check the cached default web.xml freshness with File.lastModified() for
    file: URIs instead of opening a URLConnection.
  • Use Collections.singleton for the per-fragment annotation merge.

Mapper (Mapper / batch registration)

  • Register a context's servlet mappings into the copy-on-write wrapper arrays in
    a single merge per mapping category instead of reallocating the array once per
    mapping (previously O(n²)). Verified equivalent to the old sequential
    insertMap behaviour (sorted order, existing-wins dedup, wildcard nesting)
    over 300k randomized cases.

Web resources

  • JarWarResourceSet: build the META-INF bloom filter once when the entries
    are read rather than rebuilding it on every resource lookup.
  • AbstractSingleArchiveResourceSet: pre-size the entry map from the JAR entry
    count.
  • StandardRoot: hoist a loop invariant in listResources; Collections.addAll
    instead of Arrays.asList wrapper.
  • AbstractFileResourceSet / FileResource: file() already resolves the
    canonical path to validate that a file is located under the resource set base,
    then discards it, leaving FileResource.getCanonicalPath() to resolve the same
    file a second time. The body of file() moves to a new validatedFile() that
    returns the file together with that path, and the two resource sets that create
    a FileResource pass it on. file() is kept for its other callers.

Misc internal

  • StandardJarScanner: hoist loop-invariant getJarFileURL()/toURI() out of the
    manifest Class-Path loop; Collections.addAll for the class-path URLs.
  • Digester.updateAttributes: resolve the class loader once per call.
  • FragmentJarScannerCallback: single map lookup for duplicate detection.
  • LifecycleBase.fireLifecycleEvent: skip allocating the event when there are no
    listeners.
  • AbstractProtocol.init: compute the (built/quoted) name once.
  • MbeansDescriptorsIntrospectionSource: Set membership test instead of a
    linear array scan.

Correctness

All changes preserve observable behaviour. The Mapper change was fuzz-checked
against the original sequential implementation (identical arrays and nesting).
TestContextConfigAnnotation is updated to build the new derived index (mirroring
the normal processClasses flow).

The canonical path reuse does not weaken the resource set validation: the checks
in file() are unchanged, and the path is only handed to the FileResource that
those checks just validated. It is null when getAllowLinking() short-circuits
the validation, and nothing is cached across resources or over time, so this is a
per-instance value rather than a canonical path cache. The existing 7-arg
FileResource constructor is retained as a delegate.

Please run at least TestMapper*, TestContextConfig*, the webresources tests,
and TestDefaultServlet*.

Performance

Measured on a build from source (JDK 25), median of 8 start/stop cycles, same
webapp set both sides. The gains scale with application size:

  • Mapping-heavy app (~8000 classes, 20000 servlet mappings): server startup
    ~560 ms → ~483 ms (~14%). Measured without the newest CanonicalPath cache, which even improves it more.
  • Small/typical apps: low single-digit %, near run-to-run noise.

The canonical path reuse removes one syscall per file resource lookup. On a
resource-heavy deploy (JFR, Windows, ~500 classes under WEB-INF/classes) it
eliminated FileResource.getCanonicalPath() from the profile entirely, 21 of 112
canonicalize0 samples. The effect is smaller on Linux, where path resolution is
considerably cheaper than on Windows.

Changelog entries are included.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com

@markt-asf markt-asf left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the first, simple change I reviewed missed an obvious simplification, I think a thorough review is required (which I don't have time for right now but I will try and work through this bit by bit as time allows).

It may be helpful if this PR was refactored to use one commit per issue as that would provide a natural breakdown of this large commit into more manageable chunks.

I also wonder if one PR per issue would be easier to manage / follow the review process as I suspect a PR of this size might become difficult to follow - particularly if it is reviewed/applied in stages and during that process the underlying code changes for other reasons.

Comment on lines +4736 to +4737
String[] names = findParameters();
for (String s : names) {
mergedParams.put(s, findParameter(s));
for (Map.Entry<String,String> parameter : parameters.entrySet()) {
mergedParams.put(parameter.getKey(), parameter.getValue());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could have used Map.putAll(Map).

@tandraschko

Copy link
Copy Markdown
Member Author

i did it by design in one PR as many files only contains very small changes.
Only Mapper and ContextConfig is a bigger change - i could remove it from here and create 2 additional PRs for this files. WDYT?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants